#include "network_interface.hh"

#include "arp_message.hh"
#include "ethernet_frame.hh"
#include "ethernet_header.hh"
#include "ipv4_datagram.hh"
#include "parser.hh"

#include <iostream>
#include <utility>

// Dummy implementation of a network interface
// Translates from {IP datagram, next hop address} to link-layer frame, and from link-layer frame to IP datagram

// For Lab 5, please replace with a real implementation that passes the
// automated checks run by `make check_lab5`.

// You will need to add private members to the class declaration in `network_interface.hh`

template <typename... Targs>
void DUMMY_CODE(Targs &&.../* unused */) {}

using namespace std;

//! \param[in] ethernet_address Ethernet (what ARP calls "hardware") address of the interface
//! \param[in] ip_address IP (what ARP calls "protocol") address of the interface
NetworkInterface::NetworkInterface(const EthernetAddress &ethernet_address, const Address &ip_address)
    : _ethernet_address(ethernet_address), _ip_address(ip_address) {
    cerr << "DEBUG: Network interface has Ethernet address " << to_string(_ethernet_address) << " and IP address "
         << ip_address.ip() << "\n";
}

//! \param[in] dgram the IPv4 datagram to be sent
//! \param[in] next_hop the IP address of the interface to send it to (typically a router or default gateway, but may also be another host if directly connected to the same network as the destination)
//! (Note: the Address type can be converted to a uint32_t (raw 32-bit IP address) with the Address::ipv4_numeric() method.)
void NetworkInterface::send_datagram(const InternetDatagram &dgram, const Address &next_hop) {
    // convert IP address of next hop to raw 32-bit representation (used in ARP header)
    const uint32_t next_hop_ip = next_hop.ipv4_numeric();

    EthernetFrame frame;
    frame.header().type = EthernetHeader::TYPE_IPv4;
    frame.header().src = _ethernet_address;
    frame.payload() = dgram.serialize();

    if (_cache.count(next_hop_ip)) {
        frame.header().dst = _cache[next_hop_ip];
        _frames_out.emplace(std::move(frame));
    } else {
        if (!_requested_ips.count(next_hop_ip)) {
            ARPMessage msg;
            msg.opcode = ARPMessage::OPCODE_REQUEST;
            msg.sender_ethernet_address = _ethernet_address;
            msg.sender_ip_address = _ip_address.ipv4_numeric();
            msg.target_ip_address = next_hop_ip;

            EthernetFrame request;
            request.header().type = EthernetHeader::TYPE_ARP;
            request.header().src = _ethernet_address;
            request.header().dst = ETHERNET_BROADCAST;
            request.payload() = msg.serialize();
            _frames_out.emplace(std::move(request));

            _sent_arp_request.emplace(_time, next_hop_ip);
            _requested_ips.insert(next_hop_ip);
        }
        _waiting_frames[next_hop_ip].emplace_back(std::move(frame));
    }
}

//! \param[in] frame the incoming Ethernet frame
optional<InternetDatagram> NetworkInterface::recv_frame(const EthernetFrame &frame) {
    if (frame.header().dst != _ethernet_address && frame.header().dst != ETHERNET_BROADCAST) {
        return {};
    }
    if (frame.header().type == EthernetHeader::TYPE_IPv4) {
        InternetDatagram dgram;
        if (ParseResult::NoError == dgram.parse(frame.payload())) {
            return dgram;
        }
    } else if (frame.header().type == EthernetHeader::TYPE_ARP) {
        ARPMessage msg;
        if (ParseResult::NoError == msg.parse(frame.payload())) {
            if (!_cache.count(msg.sender_ip_address)) {
                _mapped_ips.emplace(_time, msg.sender_ip_address);
                _cache[msg.sender_ip_address] = msg.sender_ethernet_address;

                for (auto &&f : _waiting_frames[msg.sender_ip_address]) {
                    f.header().dst = msg.sender_ethernet_address;
                    _frames_out.emplace(f);
                }
                _waiting_frames.erase(msg.sender_ip_address);
            }
            if (msg.opcode == ARPMessage::OPCODE_REQUEST && msg.target_ip_address == _ip_address.ipv4_numeric()) {
                ARPMessage reply;
                reply.opcode = ARPMessage::OPCODE_REPLY;
                reply.sender_ethernet_address = _ethernet_address;
                reply.sender_ip_address = _ip_address.ipv4_numeric();
                reply.target_ethernet_address = msg.sender_ethernet_address;
                reply.target_ip_address = msg.sender_ip_address;
                EthernetFrame rp;
                rp.header().src = _ethernet_address;
                rp.header().dst = msg.sender_ethernet_address;
                rp.header().type = EthernetHeader::TYPE_ARP;
                rp.payload() = reply.serialize();
                _frames_out.emplace(std::move(rp));
            }
        }
    }
    return {};
}

//! \param[in] ms_since_last_tick the number of milliseconds since the last call to this method
void NetworkInterface::tick(const size_t ms_since_last_tick) {
    _time += ms_since_last_tick;

    while (!_sent_arp_request.empty() && _sent_arp_request.front().first + 5000 <= _time) {
        _requested_ips.erase(_sent_arp_request.front().second);
        _sent_arp_request.pop();
    }

    while (!_mapped_ips.empty() && _mapped_ips.front().first + 30000 <= _time) {
        _cache.erase(_mapped_ips.front().second);
        _mapped_ips.pop();
    }
}
